#region License

// This file is part of Grawlix.
// 
// Grawlix is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// Grawlix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with Grawlix.  If not, see
// <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Globalization;

namespace Grawlix.Geometry
{
	/// <summary>
	/// An immutable rectangle with an angle of orientation. Not
	/// axis-aligned.
	/// </summary>
	/// <remarks>
	/// Like the <see cref="Rect"/>, no guarantees are made about
	/// the area of the
	/// <see cref="OrientedRect"/>. It is possible to create a
	/// malformed 
	/// <see cref="OrientedRect"/> where one coordinate is infinite
	/// or NAN. Or a maximal extent could be less than a minimal
	/// extent, leading to negative dimensions.
	/// </remarks>
	public struct OrientedRect : IEquatable<OrientedRect>
	{
		/// <summary>
		/// Center of the rectangle.
		/// </summary>
		private readonly Vector2 _centroid;

		/// <summary>
		/// The dimensions of the rectangle.
		/// </summary>
		private readonly Vector2 _dimensions;

		/// <summary>
		/// The rotation matrix of the rectangle.
		/// </summary>
		private readonly Rotation2 _rotation;

		/// <summary>
		/// Initializes an <see cref="OrientedRect"/>with specified
		/// parameters.
		/// </summary>
		/// <param name="centroid">The center point of the rectangle.
		/// </param>
		/// <param name="dimensions">The width and height of the
		/// rectangle.</param>
		/// <param name="rotation">The rotation matrix of the
		/// rectangle, which may be the zero matrix.</param>
		/// <exception cref="InvalidGeometryException">Dimensions are
		/// not 0 or positive.</exception>
		public OrientedRect(Vector2 centroid, Vector2 dimensions,
		                    Rotation2 rotation)
		{
			_centroid = centroid;
			_rotation = rotation;
			_dimensions = dimensions;

			if (!(_dimensions.X >= 0) || !(_dimensions.Y >= 0))
			{
				throw new InvalidGeometryException(
					"Unable to specify an oriented rectangle with " +
					"dimensions that are not 0 or positive. " +
					"OrientedRect specified as " + this + ".");
			}
		}

		/// <summary>
		/// An axis-aligned bounding box for the shape.
		/// </summary>
		public Rect BoundingBox
		{
			get
			{
				float sin = Math.Abs(_rotation.Sin) / 2;
				float cos = Math.Abs(_rotation.Cos) / 2;
				float x = Dimensions.X * cos + Dimensions.Y * sin;
				float y = Dimensions.X * sin + Dimensions.Y * cos;
				var halfWidths = new Vector2(x, y);
				return new Rect(_centroid - halfWidths,
				                _centroid + halfWidths);
			}
		}

		/// <summary>
		/// The centroid of the shape.
		/// </summary>
		/// <remarks>The centroid of a shape is its center of mass,
		/// given uniform density.</remarks>
		public Vector2 Centroid
		{
			get { return _centroid; }
		}

		/// <summary>
		/// The width and height of the rectangle.
		/// </summary>
		public Vector2 Dimensions
		{
			get { return _dimensions; }
		}

		/// <summary>
		/// The rotation matrix of the rectangle.
		/// </summary>
		public Rotation2 Rotation
		{
			get { return _rotation; }
		}

		/// <summary>
		/// Checks to see if a point is within the current 
		/// <see cref="Shape"/>.
		/// </summary>
		/// <param name="point">The point to check.</param>
		/// <returns>true if within, else false.</returns>
		public bool Contains(Vector2 point)
		{
			point = _rotation.Invert() * (point - _centroid);
			point = new Vector2(Math.Abs(point.X), Math.Abs(point.Y));
			return point.X <= _dimensions.X &&
			       point.Y <= _dimensions.Y;
		}

		/// <summary>
		/// Indicates whether this instance and a specified object are
		/// equal.
		/// </summary>
		/// <returns>
		/// true if <paramref name="obj" /> and this instance are the
		/// same type and represent the same value; otherwise, false.
		/// </returns>
		/// <param name="obj">Another object to compare to. </param>
		/// <remarks>Different representations of the same 
		/// <see cref="OrientedRect" /> are not considered equal.
		/// </remarks>
		public override bool Equals(object obj)
		{
			return obj.GetType() == typeof(OrientedRect) &&
			       Equals((OrientedRect)obj);
		}

		/// <summary>
		/// Indicates whether the current object is equal to another
		/// object of the same type.
		/// </summary>
		/// <returns>
		/// true if the current object is equal to the
		/// <paramref name="other" /> parameter; otherwise, false.
		/// </returns>
		/// <param name="other">An object to compare with this object.
		/// </param>
		/// <remarks>Different representations of the same 
		/// <see cref="OrientedRect" /> are not considered equal.
		/// </remarks>
		public bool Equals(OrientedRect other)
		{
			return other._centroid.Equals(_centroid) &&
			       other._dimensions.Equals(_dimensions) &&
			       other._rotation.Equals(_rotation);
		}

		/// <summary>
		/// Create an oriented rectangle given an axis-aligned rect and
		/// angle.
		/// </summary>
		/// <param name="rect">The original rectangle.</param>
		/// <returns>The oriented rectangle representation.</returns>
		public static OrientedRect FromAxisAligned(Rect rect)
		{
			return FromAxisAligned(rect, Rotation2.Identity);
		}

		/// <summary>
		/// Create an oriented rectangle given an axis-aligned rect and
		/// angle.
		/// </summary>
		/// <param name="rect">The original rectangle.</param>
		/// <param name="rotation">The rotation matrix.</param>
		/// <returns>The oriented rectangle.</returns>
		public static OrientedRect FromAxisAligned(Rect rect,
		                                           Rotation2
		                                           	rotation)
		{
			return new OrientedRect(rect.Centroid, rect.Dimensions,
			                        rotation);
		}

		/// <summary>
		/// Returns the hash code for this instance.
		/// </summary>
		/// <returns>
		/// A 32-bit signed integer that is the hash code for this
		/// instance.
		/// </returns>
		public override int GetHashCode()
		{
			unchecked
			{
				int result = _centroid.GetHashCode();
				result = (result * 397) ^ _dimensions.GetHashCode();
				result = (result * 397) ^ _rotation.GetHashCode();
				return result;
			}
		}

		/// <summary>
		/// Normalize the <see cref="OrientedRect"/> so that rotation
		/// is within [0, 90) degrees. A zero rotation or zero
		/// dimensions will result in zero dimensions with the zero
		/// rotation.
		/// </summary>
		/// <returns>
		/// A normalized <see cref="OrientedRect"/>.
		/// </returns>
		/// <remarks>The normalization is only for comparison, which,
		/// with floating point, is only useful for test cases.
		/// </remarks>
		public OrientedRect Normalize()
		{
			if (_rotation == Rotation2.Zero ||
			    _dimensions == Vector2.Zero)
			{
				return new OrientedRect(_centroid, Vector2.Zero,
				                        Rotation2.Zero);
			}

			Rotation2 rotation = _rotation;
			Vector2 dimensions = _dimensions;

			// Normalize to [0, 180)
			if (rotation.Sin <= 0 && rotation.Cos != 1)
			{
				rotation = Rotation2.FromCosSin(-rotation.Cos,
				                                -rotation.Sin);
			}

			// Normalize to [0, 90)
			if (rotation.Cos <= 0 && rotation.Sin != 0)
			{
				dimensions = new Vector2(dimensions.Y, dimensions.X);
				rotation = Rotation2.FromCosSin(rotation.Sin,
				                                -rotation.Cos);
			}

			return new OrientedRect(_centroid, dimensions, rotation);
		}

		/// <summary>
		/// Obtains a string representation of this object.
		/// </summary>
		/// <returns>A string representation of this object.</returns>
		public override string ToString()
		{
			return string.Format(CultureInfo.InvariantCulture,
			                     "Centroid: {0}, Dimensions: {1}, Rotation: {2}",
			                     _centroid, _dimensions, _rotation);
		}

		/// <summary>
		/// Finds an axis-aligned bounding box for the shape after
		/// applying a <see cref="Transform2"/>.
		/// </summary>
		/// <param name="transformToApply">The transform to apply.</param>
		/// <returns>The axis-aligned bounding box.</returns>
		public OrientedRect Transform(Transform2 transformToApply)
		{
			Rotation2 rotation = transformToApply.Rotation *
			                     _rotation;
			Vector2 centroid = transformToApply.Apply(_centroid);
			return new OrientedRect(centroid, _dimensions, rotation);
		}

		/// <summary>
		/// Determines whether two <see cref="OrientedRect"/> objects
		/// have the same value.
		/// </summary>
		/// <param name="left">A <see cref="OrientedRect"/>.</param>
		/// <param name="right">A <see cref="OrientedRect"/>.</param>
		/// <returns>true if the value of <paramref name="left"/> is
		/// the same as <paramref name="right"/>; else false.</returns>
		/// <remarks>Different representations of the same 
		/// <see cref="OrientedRect" /> are not considered equal.
		/// </remarks>
		public static bool operator ==(
			OrientedRect left, OrientedRect right)
		{
			return left.Equals(right);
		}

		/// <summary>
		/// Determines whether two <see cref="OrientedRect"/> objects
		/// have different values.
		/// </summary>
		/// <param name="left">A <see cref="OrientedRect"/>.</param>
		/// <param name="right">A <see cref="OrientedRect"/>.</param>
		/// <returns>true if the value of <paramref name="left"/> is
		/// different from <paramref name="right"/>; else false.
		/// </returns>
		/// <remarks>Different representations of the same 
		/// <see cref="OrientedRect" /> are not considered equal.
		/// </remarks>
		public static bool operator !=(
			OrientedRect left, OrientedRect right)
		{
			return !left.Equals(right);
		}
	}
}